package com.zxj.friend.service.user.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.repository.AbstractRepository;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zxj.common.core.contains.CacheConstants;
import com.zxj.common.core.contains.Constants;
import com.zxj.common.core.contains.HttpConstants;
import com.zxj.common.core.domain.R;
import com.zxj.common.core.domain.cache.LoginUserCache;
import com.zxj.common.core.enums.ResultCode;
import com.zxj.common.core.enums.UserStatus;
import com.zxj.common.core.utils.ThreadLocalUtil;
import com.zxj.common.message.service.AliSmsService;
import com.zxj.common.redis.service.RedisService;
import com.zxj.common.security.exception.ServiceException;
import com.zxj.common.security.service.TokenService;
import com.zxj.friend.domain.user.dto.UserCodeDTO;
import com.zxj.friend.domain.user.dto.UserLoginDTO;
import com.zxj.friend.domain.user.dto.UserUpdateDTO;
import com.zxj.friend.domain.user.entity.User;
import com.zxj.friend.domain.user.entity.UserSubmit;
import com.zxj.friend.domain.user.vo.LoginUserVO;
import com.zxj.friend.domain.user.vo.UserLoginVO;
import com.zxj.friend.domain.user.vo.UserVO;
import com.zxj.friend.manager.UserCacheManager;
import com.zxj.friend.mapper.user.UserMapper;
import com.zxj.friend.service.user.IUserService;
import jakarta.annotation.PostConstruct;
import jakarta.validation.constraints.NotEmpty;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

/**
 * <p>
 * 用户表 服务实现类
 * </p>
 *
 * @author zxj2022
 * @since 2025-01-01
 */
@Service
@Slf4j
@RequiredArgsConstructor
@RefreshScope
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    private final RedisService redisService;
    private final AliSmsService aliSmsService;
    private final TokenService tokenService;
    private final UserCacheManager userCacheManager;

    @Value("${sms.code-expiration:5}")
    private Long phoneCodeExpiration;

    @Value("${sms.send-limit:3}")
    private Integer sendLimit;

    @Value("${sms.is-send:false}")
    private boolean isSend;  //开关打开：true  开关关闭false

    @Value("${oj.jwt.token.secret}")
    private String secret; // jwt密钥

    @Value("${file.oss.downloadUrl}")
    private String downloadUrl;

    /**
     * 发送验证码
     *
     * @param userCodeDTO 用户请求发生验证码参数
     * @return 结果
     */
    @Override
    public boolean sendCode(UserCodeDTO userCodeDTO) {
        // 1. 验证手机号是否正确
        if (!checkPhone(userCodeDTO.getPhone())) {
            log.error("手机号格式错误：{}", userCodeDTO.getPhone());
            throw new ServiceException(ResultCode.FAILED_USER_PHONE);
        }
        // 2. 该手机验证码的发送频率是否正常 - 发送验证码间隔不能小于 phoneCodeExpiration - 1 分钟
        String phoneCodeKey = this.getPhoneCodeKey(userCodeDTO.getPhone());
        Long expire = redisService.getExpire(phoneCodeKey, TimeUnit.SECONDS);
        if (expire != null && phoneCodeExpiration * 60 - expire < 60) {
            // 发送验证码间隔不能小于 phoneCodeExpiration - 1 分钟
            log.error("发送验证码频率过快：{}", userCodeDTO.getPhone());
            throw new ServiceException(ResultCode.FAILED_FREQUENT);
        }
        // 3. 当天发送验证码次数是否超过限制
        String codeTimeKey = this.getCodeTimeKey(userCodeDTO.getPhone()); // 获取缓存key
        Long sendTimes = redisService.getCacheObject(codeTimeKey, Long.class); // 获取当日发送次数
        if (sendTimes != null && sendTimes >= sendLimit) { // 当日发送次数超过限制
            log.error("发送验证码当日次数超过限制：{}", userCodeDTO.getPhone());
            throw new ServiceException(ResultCode.FAILED_TIME_LIMIT);
        }
        // 4. 发送验证码
        // 4.1 生成验证码, isSend为false时, 则表示是测试环境, 验证码为默认值; 为true时, 表示生成环境, 生成6位随机数. 这样做的目的是方便后续测试
        String code = isSend ? RandomUtil.randomNumbers(6) : Constants.DEFAULT_CODE;
        log.debug("发送验证码：{} -> {}", userCodeDTO.getPhone(), code);
        // 4.2 将验证码存入redis, 有效时间为phoneCodeExpiration分钟
        redisService.setCacheObject(phoneCodeKey, code, phoneCodeExpiration, TimeUnit.MINUTES);
        // 4.3 发送验证码, isSend为true时, 发送验证码; 为false时, 不发送验证码
        if (isSend) {
            boolean sendMobileCode = aliSmsService.sendMobileCode(userCodeDTO.getPhone(), code, String.valueOf(phoneCodeExpiration));
            if (!sendMobileCode) {
                log.error("发送验证码失败：{}", userCodeDTO.getPhone());
                throw new ServiceException(ResultCode.FAILED_SEND_CODE);
            }
        }

        // 5. 记录当天发送验证码次数
        redisService.increment(codeTimeKey);
        if (sendTimes == null) {  //说明是当天第一次发起获取验证码的请求
            // 设置当天剩余有效时间
            long seconds = ChronoUnit.SECONDS.between(LocalDateTime.now(),
                    LocalDateTime.now().plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0));
            redisService.expire(codeTimeKey, seconds, TimeUnit.SECONDS);
        }

        return true;
    }

    /**
     * 用户登录或注册
     *
     * @param userLoginDTO 用户登录参数
     * @return 用户登录结果
     */
    @Override
    public UserLoginVO codeLogin(UserLoginDTO userLoginDTO) {
        // 1. 验证手机号是否正确
        if (!checkPhone(userLoginDTO.getPhone())) {
            log.error("手机号格式错误：{}", userLoginDTO.getPhone());
            throw new ServiceException(ResultCode.FAILED_USER_PHONE);
        }
        // 2. 验证验证码是否正确
        String phoneCodeKey = this.getPhoneCodeKey(userLoginDTO.getPhone());
        String cacheCode = redisService.getCacheObject(phoneCodeKey, String.class);

        if (StrUtil.isEmpty(cacheCode) || !cacheCode.equals(userLoginDTO.getCode())) {
            log.error("验证码错误：{}", userLoginDTO.getCode());
            throw new ServiceException(ResultCode.FAILED_ERROR_CODE);
        }

        // 3. 验证通过, 删除验证码
        redisService.deleteObject(phoneCodeKey);

        // 4. 查询用户是否存在
        User user = this.lambdaQuery().eq(User::getPhone, userLoginDTO.getPhone()).one();
        if (user == null) {  //新用户
            //注册逻辑
            user = new User();
            user.setPhone(userLoginDTO.getPhone());
            user.setStatus(UserStatus.Normal.getValue());
            user.setCreateBy(Constants.SYSTEM_USER_ID);
            user.setNickName(Constants.USER_DEFAULT_NAME_PREFIX + RandomUtil.randomNumbers(6));
            user.setHeadImage(Constants.USER_DEFAULT_HEAD_IMAGE);
            this.save(user);
        }

        // 5. 生成token
        return UserLoginVO.builder()
                .token(tokenService.createToken(user.getUserId(), secret, UserStatus.Normal.getValue(), user.getNickName(), user.getHeadImage()))
                .build();
    }


    /**
     * 退出登录
     *
     * @param token token
     * @return 成功与否结果
     */
    @Override
    public boolean logout(String token) {
        if (StrUtil.isNotEmpty(token) && token.startsWith(HttpConstants.PREFIX)) {
            token = token.replaceFirst(HttpConstants.PREFIX, StrUtil.EMPTY);
        }
        return tokenService.deleteLoginUser(token, secret);
    }


    /**
     * 从 token 中获取用户信息
     *
     * @param token token
     * @return 用户信息
     */
    @Override
    public LoginUserVO info(String token) {
        if (StrUtil.isNotEmpty(token) && token.startsWith(HttpConstants.PREFIX)) {
            token = token.replaceFirst(HttpConstants.PREFIX, StrUtil.EMPTY);
        }
        LoginUserCache loginUserCache = tokenService.getLoginUserCacheFromToken(token, secret);
        if (loginUserCache == null) {
            log.error("用户信息不存在：{}", token);
            throw new ServiceException(ResultCode.FAILED_UNAUTHORIZED);
        }
        LoginUserVO loginUserVO = new LoginUserVO();
        loginUserVO.setNickName(loginUserCache.getNickName());
        if (StrUtil.isNotEmpty(loginUserCache.getHeadImage())) {
            loginUserVO.setHeadImage(downloadUrl + loginUserCache.getHeadImage());
        }
        return loginUserVO;
    }


    /**
     * 获取用户详情
     *
     * @return 用户详情
     */
    @Override
    public UserVO detail() {
        // 从ThreadLocal中获取用户id
        Long userId = ThreadLocalUtil.getUserId();
        if (userId == null) {
            // 用户未登录, 抛出异常
            throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);
        }
        // 从缓存中获取用户信息
        UserVO userVO = userCacheManager.getUserById(userId);
        if (userVO == null) {
            // 缓存中不存在用户信息, 抛出异常
            throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);
        }
        if (StrUtil.isNotEmpty(userVO.getHeadImage())) {
            // 数据库中存储的是头像唯一标识, 要正确访问需要拼接头像地址
            userVO.setHeadImage(downloadUrl + userVO.getHeadImage());
        }
        return userVO;
    }

    /**
     * 编辑用户信息
     * @param userUpdateDTO 用户更新信息
     * @return 结果
     */
    @Override
    @Transactional
    public boolean edit(UserUpdateDTO userUpdateDTO) {
        // 1. 要更新数据库
        // 2. 要更新缓存, 登录用户信息缓存和用户详情缓存
        Long userId = ThreadLocalUtil.getUserId();
        if (userId == null) {
            throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);
        }
        User user = this.getById(userId);
        if (user == null) {
            throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);
        }
        if (userUpdateDTO.getHeadImage() != null) {
            userUpdateDTO.setHeadImage(FileUtil.getName(userUpdateDTO.getHeadImage()));
        }
        BeanUtil.copyProperties(userUpdateDTO, user, CopyOptions.create().setIgnoreNullValue(true));
        user.setUpdateBy(userId);
        user.setUpdateTime(LocalDateTime.now());
        // 更新数据库
        boolean b = this.updateById(user);
        if (!b) {
            log.error("数据库更新用户信息失败：{}", userUpdateDTO);
            throw new ServiceException(ResultCode.FAILED_FREQUENT);
        }
        //更新用户缓存
        userCacheManager.refreshUser(user);
        tokenService.refreshLoginUser(user.getNickName(), user.getHeadImage(),
                ThreadLocalUtil.getUserKey());
        return true;
    }

    @Override
    @Transactional
    public boolean updateHeadImage(String headImage) {
        return edit(UserUpdateDTO.builder().headImage(headImage).build());
    }

    /**
     * 验证手机号是否正确
     * @param phone 手机号
     * @return 结果
     */
    private boolean checkPhone(@NotEmpty(message = "手机号不能为空") String phone) {
        String regex = "^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[189]))\\d{8}$";
        return phone.matches(regex);
    }


    /**
     * 获取手机验证码key, p:c:${phone}
     *
     * @param phone 手机号
     * @return 加上前缀缓存key
     */
    private String getPhoneCodeKey(String phone) {
        return CacheConstants.PHONE_CODE_KEY + phone;
    }

    /**
     * 获取手机验证码请求次数key, c:t:${phone}
     *
     * @param phone 手机号
     * @return 加上前缀缓存key
     */
    private String getCodeTimeKey(String phone) {
        return CacheConstants.CODE_TIME_KEY + phone;
    }
}
